package com.andengine.editor.atlaseditor;

import com.andengine.editor.Utils;
import com.andengine.editor.data.TextureFile;
import com.andengine.editor.factories.AtlasFactory;
import com.gooddies.exceptions.ValidationException;
import com.gooddies.swing.Toast;
import com.gooddies.swing.hComboBox;
import com.gooddies.swing.hList;
import com.swingson.Swingson;
import com.swingson.SwingsonGui;
import com.swingson.SwingsonGuiBuilder;
import com.swingson.easydrag.DefaultDrag;
import com.swingson.easydrag.DefaultDrop;
import com.swingson.easydrag.listeners.DragListener;
import com.swingson.easydrag.listeners.DropListener;
import com.swingson.exceptions.ErrorMessageException;
import com.swingson.properties.specificclass_impl.MouseActionPropertyProcessor.Button;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToolTip;
import javax.swing.SwingUtilities;

/**
 * @author sad
 */
public class TextureAtlasEditorPanel extends JPanel {

    private static final Integer[] TEXTURE_SIZES = new Integer[]{4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048};
    private final hList<TextureFileWithUsage> texturesList = new hList<TextureFileWithUsage>() {
        private String tooltip;

        @Override
        public JToolTip createToolTip() {
            return new ImageTooltip(tooltip);
        }

        @Override
        public String getToolTipText(MouseEvent event) {
            int index = texturesList.locationToIndex(event.getPoint());
            if (index != -1) {
                return tooltip = texturesList.getItem(index).getTextureFile().getAbsolutePath();
            } else {
                tooltip = null;
                return null;
            }
        }
    };

    private final hComboBox<TextureOptions> textureOptionsComboBox = new hComboBox<>(TextureOptions.values());
    private final JComboBox<Integer> textureWidthComboBox = new hComboBox<Integer>(TEXTURE_SIZES);
    private final hComboBox<Integer> textureHeightComboBox = new hComboBox<Integer>(TEXTURE_SIZES);
    private final JScrollPane scrollPane = new JScrollPane();
    private int atlasWidth = 512;
    private int atlasHeight = 512;
    private int xOffset;
    private int yOffset;
    private final List<TextureInAtlas> atlasTextures = new ArrayList<TextureInAtlas>();
    private TextureInAtlas selectedTexture;
    private boolean isDragged = false;
    private int draggedDx = 0;
    private int draggedDy = 0;
    private Color textureBorder = new Color(240, 240, 240);
    private boolean modified = false;
    private SwingsonGui gui;
    private final JPanel atlasCanvas = new JPanel() {
        @Override
        public void paint(Graphics g) {
            super.paint(g);
            drawPanel(g);
        }
    };

    public TextureAtlasEditorPanel() {
        setPreferredSize(new Dimension(800, 600));
        scrollPane.getViewport().add(atlasCanvas);
        textureWidthComboBox.setSelectedItem(512);
        textureHeightComboBox.setSelectedItem(512);
        gui = SwingsonGuiBuilder.createGuiFromJsonInPackage("textureAtlasEditor.json", this);
        texturesList.setCellRenderer(new DefaultListCellRenderer(){

            @Override
            public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
                JComponent component=(JComponent) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                TextureFileWithUsage tf=(TextureFileWithUsage) value;
                if(tf.getUsage()==0){
                    component.setForeground(Color.lightGray);
                }else{
                    component.setForeground(Color.BLACK);
                }
                return component;
            }
        
        });
        DefaultDrag drag = new DefaultDrag(texturesList.getJList(), new DragListener() {

            @Override
            public Object getObject(DragGestureEvent de) {
                Object obj = texturesList.getSelectedItem();
                return obj != null ? ((TextureFileWithUsage) obj) : null;
            }
        });

        drag.setDragVisible(true);
        drag.setEnabled(true);
        DefaultDrop drop = new DefaultDrop(atlasCanvas, new DropListener() {
            @Override
            public boolean canDrop(Component source, Component target, Object data, int x, int y) {
                return (data instanceof TextureFileWithUsage) && isCoordsInAtlasTexture(x, y);
            }

            @Override
            public void performDrop(Component source, Component target, DropTargetDropEvent dtde, Object data) {
                onTextureDropToAtlas((TextureFileWithUsage) data, dtde.getLocation().x, dtde.getLocation().y);
            }
        });

        drop.setEnabled(true);
        scrollPane.setFocusable(true);
        atlasCanvas.setFocusable(true);
        texturesList.setFocusable(true);
        recalculateTextureAtlas();
        clearModified();
    }

    public void setModified() {
        modified = true;
        ((JFrame) SwingUtilities.getWindowAncestor(this)).setTitle("Texture Atlas Editor *");
    }

    public void clearModified() {
        modified = false;
        JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
        if (frame != null) {
            frame.setTitle("Texture Atlas Editor");
        }
    }

    public boolean isModified() {
        return modified;
    }

    protected void textureListKeyPressed(KeyEvent keyEvent) {
        if (keyEvent.getKeyCode() == KeyEvent.VK_DELETE) {
            int selectedObject = texturesList.getSelectedIndex();
            if (selectedObject != -1) {
                TextureFile file = texturesList.getSelectedItem().getTextureFile();
                texturesList.removeObject(selectedObject);
                for (int i = 0; i < atlasTextures.size(); i++) {
                    TextureInAtlas texture = atlasTextures.get(i);
                    if (texture.getTextureFile() == file) {
                        atlasTextures.remove(i);
                        i--;
                    }
                }

                setModified();
            }

            atlasRepaint();
        }
    }

    protected void atlasKeyPressed(KeyEvent keyEvent) {
        switch (keyEvent.getKeyCode()) {
            case KeyEvent.VK_DELETE:
                deleteSelectedTexture();
                break;
            case KeyEvent.VK_UP:
            case KeyEvent.VK_DOWN:
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_RIGHT:
                moveSelectedTexture(keyEvent.getKeyCode());
                break;
        }
    }

    private void moveSelectedTexture(int key) {
        if (selectedTexture != null) {
            switch (key) {
                case KeyEvent.VK_UP:
                    selectedTexture.setY(selectedTexture.getY() - 1);
                    break;
                case KeyEvent.VK_DOWN:
                    selectedTexture.setY(selectedTexture.getY() + 1);
                    break;
                case KeyEvent.VK_LEFT:
                    selectedTexture.setX(selectedTexture.getX() - 1);
                    break;
                case KeyEvent.VK_RIGHT:
                    selectedTexture.setX(selectedTexture.getX() + 1);
                    break;
            }
            selectedTexture.setCollision(checkTextureCollision(selectedTexture));
            checkNotSelectedButCollidedTextures(selectedTexture);
            setStatusMessageAboutTexture(selectedTexture);
            setModified();
            atlasRepaint();
        }
    }

    private void deleteSelectedTexture() {
        if (selectedTexture != null) {
            int index = atlasTextures.indexOf(selectedTexture);
            if (index != -1) {
                atlasTextures.remove(index);
                atlasRepaint();
                setModified();
                for (int i = 0; i < texturesList.getItemCount(); i++) {
                    TextureFileWithUsage texture = texturesList.getItem(i);
                    if (texture.getTextureFile() == selectedTexture.getTextureFile()) {
                        texture.setUsage(texture.getUsage() - 1);
                        break;
                    }
                }
            }
        }
    }

    protected void onError(Throwable ex) {
        if (ex instanceof ErrorMessageException) {
            Toast.makeText(this, ex.getMessage(), Toast.Style.ERROR).display();
        }
    }

    protected void onTextureAtlasFileDrop(File[] files) {
        for (File file : files) {
            String filePath = file.getAbsolutePath();
            if (TextureFile.isFileInAssetFolder(filePath) && TextureFile.isFileTexture(filePath)) {
                try {
                    TextureFile tf = new TextureFile(filePath);
                    for (int i = 0; i < texturesList.getItemCount(); i++) {
                        if (tf.equals(texturesList.getItem(i).getTextureFile())) {
                            throw new ErrorMessageException("Texture " + filePath + " already added to list");
                        }
                    }

                    texturesList.addItem(new TextureFileWithUsage(tf, 0));
                } catch (ErrorMessageException ex) {
                    Toast.makeText(this, ex.getMessage(), Toast.Style.ERROR).display();
                }
            }else{
                throw new ValidationException("Texture should be in assets folder");
            }
        }
        setModified();
    }

    protected void onTextureDropToAtlas(TextureFileWithUsage tf, int x, int y) {
        tf.setUsage(tf.getUsage() + 1);
        addTextureToAtlas(tf.getTextureFile(), x - xOffset, y - yOffset);
        setModified();
        atlasRepaint();
    }

    private void addTextureToAtlas(TextureFile tf, int x, int y) {
        TextureInAtlas t = new TextureInAtlas(tf, x, y);
        atlasTextures.add(t);
        setSelectedTexture(t);
        t.setCollision(checkTextureCollision(t));
    }

    private void setAtlasSize(Dimension d) {
        atlasWidth = d.width;
        atlasHeight = d.height;
        d.width = d.width + 20;
        d.height = d.height + 20;
        atlasCanvas.setPreferredSize(d);
        scrollPane.revalidate();
        scrollPane.validate();
        recalculateTextureAtlas();
    }

    private void recalculateTextureAtlas() {
        xOffset = atlasCanvas.getWidth() / 2 - atlasWidth / 2;
        yOffset = atlasCanvas.getHeight() / 2 - atlasHeight / 2;
        atlasFullRepaint();
        scrollPane.getVerticalScrollBar().setUnitIncrement(atlasHeight / 50);
    }

    protected void onAtlasResized() {
        recalculateTextureAtlas();
    }

    public void onWidthSelected(Object value) {
        setAtlasSize(new Dimension((int) value, atlasHeight));
        setModified();
        checkCollisionsOnAllElements();
    }

    public void onHeightSelected(Object value) {
        setAtlasSize(new Dimension(atlasWidth, (int) value));
        setModified();
        checkCollisionsOnAllElements();
    }

    private void checkCollisionsOnAllElements() {
        for (TextureInAtlas t : atlasTextures) {
            t.setCollision(checkTextureCollision(t));
        }
    }

    private boolean isCoordsInAtlasTexture(int x, int y) {
        if (x < xOffset || y < yOffset) {
            return false;
        }

        return x <= (xOffset + atlasWidth) && y <= (yOffset + atlasHeight);
    }

    private TextureInAtlas searchTextureByCoords(int x, int y) {
        for (int i = atlasTextures.size() - 1; i >= 0; i--) {
            TextureInAtlas t = atlasTextures.get(i);
            if ((t.getX() <= x && (t.getX() + t.getWidth()) >= x)
                    && (t.getY() <= y && (t.getY() + t.getHeight()) >= y)) {
                return t;
            }
        }

        return null;
    }

    private void setSelectedTexture(TextureInAtlas t) {
        selectedTexture = t;
        setStatusMessageAboutTexture(t);
    }

    private void setStatusMessageAboutTexture(TextureInAtlas texture) {
        if (texture == null) {
            setStatusMessage("");
        } else {
            setStatusMessage(texture.getTextureFile().getSmallPath() + " X:" + texture.getX() + ",Y:" + texture.getY());
        }
    }

    private void setStatusMessage(String message) {
        ((JLabel) gui.getDefinedComponent("statusLabel").getComponent()).setText(message);
    }

    protected void atlastMouseDown(Point p, Button button) {
        atlasCanvas.requestFocus();
        Point coords = convertCoordinates(p);
        TextureInAtlas texture = searchTextureByCoords(coords.x, coords.y);
        if (texture != null) {
            setSelectedTexture(texture);
            draggedDx = coords.x - texture.getX();
            draggedDy = coords.y - texture.getY();
            isDragged = true;
        } else {
            setSelectedTexture(null);
        }

        atlasRepaint();
    }

    protected void atlastMouseMoved(Point p, Button button) {
        Point coords = convertCoordinates(p);
        if (isDragged) {
            selectedTexture.setX(coords.x - draggedDx);
            selectedTexture.setY(coords.y - draggedDy);
            selectedTexture.setCollision(checkTextureCollision(selectedTexture));
            checkNotSelectedButCollidedTextures(selectedTexture);
            setStatusMessageAboutTexture(selectedTexture);
            setModified();
            atlasRepaint();
        }
    }

    private void checkNotSelectedButCollidedTextures(TextureInAtlas selected) {
        for (TextureInAtlas texture : atlasTextures) {
            if (texture.isCollision() && texture != selected) {
                texture.setCollision(checkTextureCollision(texture));
            }
        }
    }

    private boolean checkTextureCollision(TextureInAtlas tia) {
        boolean collided = false;
        if (tia.getX() < 0 || tia.getX2() > atlasWidth - 1 || tia.getY() < 0 || tia.getY2() > atlasHeight - 1) {
            return true;
        }

        for (TextureInAtlas texture : atlasTextures) {
            if (texture != tia) {
                if (Utils.intersectRect(tia.getX(), tia.getX2(), tia.getY(), tia.getY2(),
                        texture.getX(), texture.getX2(), texture.getY(), texture.getY2())) {
                    texture.setCollision(true);
                    collided = true;
                }
            }
        }

        return collided;
    }

    protected void atlastMouseReleased() {
        isDragged = false;
        atlasRepaint();
    }

    private Point convertCoordinates(Point p) {
        return new Point(p.x - xOffset, p.y - yOffset);
    }

    private void atlasRepaint() {
        atlasCanvas.repaint();
    }

    private void atlasFullRepaint() {
        atlasCanvas.repaint();
    }

    private void drawPanel(Graphics g) {
        g.setColor(Color.WHITE);
        g.fillRect(xOffset, yOffset, atlasWidth, atlasHeight);
        g.setColor(Color.BLACK);
        g.drawRect(xOffset - 1, yOffset - 1, atlasWidth + 1, atlasHeight + 1);
        g.setColor(new Color(50, 50, 50));
        g.fillRect(xOffset + atlasWidth + 1, yOffset + 3, 3, atlasHeight);
        g.fillRect(xOffset + 3, yOffset + atlasHeight + 1, atlasWidth + 1, 3);
        for (TextureInAtlas texture : atlasTextures) {
            g.drawImage(texture.getImage(), texture.getX() + xOffset, texture.getY() + yOffset, this);
            if (texture.isCollision()) {
                g.setColor(Color.RED);
            } else {
                if (texture == selectedTexture) {
                    g.setColor(Color.BLACK);
                } else {
                    g.setColor(textureBorder);
                }
            }

            g.drawRect(texture.getX() + xOffset, texture.getY() + yOffset, texture.getWidth(), texture.getHeight());
        }
    }

    private Atlas formatSerializedAtlas() {
        Atlas ss = new Atlas();
        ss.setTextureOptions(textureOptionsComboBox.getSelectedItem());
        ss.setAtlasHeight(atlasHeight);
        ss.setAtlasWidth(atlasWidth);
        HashMap<TextureFile, Integer> textureToIndexMap = new HashMap<>();
        List<String> addedTextures = new ArrayList<>();
        for (int i = 0; i < texturesList.getItemCount(); i++) {
            TextureFile t = texturesList.getItem(i).getTextureFile();
            addedTextures.add(t.getSmallPath());
            textureToIndexMap.put(t, i);
        }

        ss.setAddedTextures(addedTextures);
        List<Atlas.SerializedTexture> serializedTextures = new ArrayList<>();
        for (TextureInAtlas t : atlasTextures) {
            int index = textureToIndexMap.get(t.getTextureFile());
            Atlas.SerializedTexture st = new Atlas.SerializedTexture();
            st.setIndex(index);
            st.setX(t.getX());
            st.setY(t.getY());
            serializedTextures.add(st);
        }

        ss.setTextures(serializedTextures);
        return ss;
    }

    private void processSerializedAtlas(Atlas scene) {
        try {
            Swingson.disableUserInput();
            texturesList.clearItems();
            setSelectedTexture(null);
            isDragged = false;
            atlasTextures.clear();
            textureOptionsComboBox.setSelectedItem(scene.getTextureOptions());
            textureHeightComboBox.setSelectedItem(scene.getAtlasHeight());
            textureWidthComboBox.setSelectedItem(scene.getAtlasWidth());
            setAtlasSize(new Dimension(scene.getAtlasWidth(), scene.getAtlasHeight()));
            for (String filePath : scene.getAddedTextures()) {
                TextureFile file = new TextureFile(filePath);
                texturesList.addItem(new TextureFileWithUsage(file, 0));
            }

            for (Atlas.SerializedTexture st : scene.getTextures()) {
                TextureFileWithUsage textureWithUsage= texturesList.getItem(st.getIndex());
                textureWithUsage.setUsage(textureWithUsage.getUsage()+1);
                TextureFile textureFile = textureWithUsage.getTextureFile();
                addTextureToAtlas(textureFile, st.getX(), st.getY());
            }
        } finally {
            Swingson.enableUserInput();
        }
    }

    protected void onLoad() throws FileNotFoundException {
        Atlas atlas = AtlasFactory.get().read("myatlas");
        processSerializedAtlas(atlas);
        clearModified();
    }

    protected void onSave() throws FileNotFoundException {
        AtlasFactory.get().save("myatlas", formatSerializedAtlas());
        clearModified();
    }
}
